/*
 * Created by Christopher King
 * Copyright (C) 2007 Christopher King, All Rights Reserved.
 */

package us.cirion.adventure.engine;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/**
 * The Game class bears primary responsibility for the overall state of the game
 * engine. It does not include any user interface functionality, but maintains
 * the present game state and an understanding of all possible actions in the
 * game.
 * 
 * @author Chris
 */
public class Game
{

	/**
	 * The text to use in game files when referring to the player's inventory.
	 */
	public static final String INVENTORY_STRING = "inventory";
	/**
	 * The text to use in game files when referring to the player's current
	 * locatin.
	 */
	public static final String CURRENT_ROOM_STRING = "currentRoom";
	/**
	 * The text to use in game files when referring in a generic way to any
	 * item.
	 */
	public static final String ANY_ITEM_STRING = "anyItem";

	private int version;
	private String id;

	private Hashtable directions;
	private Hashtable rooms;
	private Hashtable items;
	private Hashtable verbs;

	private Player player;

	// TODO: There should be a better way to organize this
	// data. Try indexing by verb or something.
	private Vector scriptActions;

	private Vector sortedVerbs;

	// Cache aside the currently used Noun in case an action needs to reference
	// it.
	private transient Noun noun;

	/**
	 * Constructs a new Game instance.
	 */
	public Game()
	{
		directions = new Hashtable();
		rooms = new Hashtable();
		items = new Hashtable();
		verbs = new Hashtable();
		scriptActions = new Vector();
		player = new Player();
	}

	/**
	 * Returns the Player for this Game.
	 * 
	 * @return The Player instance used in this Game.
	 */
	public Player getPlayer()
	{
		return player;
	}

	/**
	 * Adds a new possible Direction to the game. Directions must be added here
	 * before they can be used to link together Rooms.
	 * 
	 * @param direction A Direction that can be taken by the Player.
	 */
	public void addDirection(Direction direction)
	{
		directions.put(direction.toString(), direction);
	}

	/**
	 * Retrieves the Direction with the indicated name.
	 * 
	 * @param direction The name of the direction.
	 * @return The Direction corresponding with the provided name.
	 */
	public Direction getDirection(String direction)
	{
		return (Direction) directions.get(direction);
	}

	/**
	 * Retrieves a collection of all the Directions known to the Game.
	 * 
	 * @return All the Directions that have been added to the Game.
	 */
	public Enumeration getDirections()
	{
		return directions.elements();
	}

	/**
	 * Adds a Room to the game.
	 * 
	 * @param room The Room to be added.
	 */
	public void addRoom(Room room)
	{
		String key = room.getName();
		rooms.put(key, room);
	}

	/**
	 * Retrieves the Room with the indicated name.
	 * 
	 * @param roomName The unique name for a room.
	 * @return The Room object that corresponds to the provided name.
	 */
	public Room getRoom(String roomName)
	{
		return (Room) rooms.get(roomName);
	}

	/**
	 * Adds the provided Noun to the game. Nouns must be added before they can
	 * be moved or manipulated through script actions.
	 * 
	 * @param item The Noun to add.
	 */
	public void addNoun(Noun item)
	{
		items.put(item.toString(), item);
	}

	/**
	 * Retrieves the Noun with the indicated name.
	 * 
	 * @param name The unique name for a Noun.
	 * @return The Noun object that corresponds to the provided name.
	 */
	public Noun getNoun(String name)
	{
		return (Noun) items.get(name);
	}

	/**
	 * Adds the provided Verb to the game.
	 * 
	 * @param verb The Verb to add.
	 */
	public void addVerb(Verb verb)
	{
		verbs.put(verb.toString(), verb);
	}

	/**
	 * Retrieves the Verb with the indicated name.
	 * 
	 * @param verbName The unique name for a Verb.
	 * @return The Verb object that corresponds to the provided name.
	 */
	public Verb getVerb(String verbName)
	{
		return (Verb) verbs.get(verbName);
	}

	/**
	 * Retrieves a collection of all the Verbs known to the Game.
	 * 
	 * @return All the Verbs in the Game, in sorted order.
	 */
	public Enumeration getVerbs()
	{
		if (sortedVerbs == null || sortedVerbs.size() != verbs.size())
		{
			int size = verbs.size();
			sortedVerbs = new Vector(verbs.size());
			sortedVerbs.setSize(size);
			Enumeration verbWalker = verbs.elements();
			while (verbWalker.hasMoreElements())
			{
				Verb verb = (Verb) verbWalker.nextElement();
				sortedVerbs.setElementAt(verb, verb.getVerbOrder());
			}
		}

		return sortedVerbs.elements();
	}

	/**
	 * Inserts a script action into the game. This describes a possible action
	 * that can be taken by the player, along with the results of that action.
	 * 
	 * @param action The script action to be added.
	 */
	public void addScriptAction(ScriptAction action)
	{
		scriptActions.addElement(action);
	}

	/**
	 * Attempt to execute an action using only the provided Verb.
	 * 
	 * @param verb The action taken by the player.
	 * @return <code>true</code> if a valid action was found and executed,
	 *         <code>false</code> otherwise.
	 */
	public boolean executeScriptAction(Verb verb)
	{
		return executeScriptAction(verb, null, null);
	}

	/**
	 * Attempt to execute a Verb-Noun action.
	 * 
	 * @param verb The action taken by the player.
	 * @param noun The item on which the action is being attempted.
	 * @return <code>true</code> if a valid action was found and executed,
	 *         <code>false</code> otherwise.
	 */
	public boolean executeScriptAction(Verb verb, Noun noun)
	{
		return executeScriptAction(verb, noun, null);
	}

	/**
	 * Attempt to execute a Verb-Noun-Noun action.
	 * 
	 * @param verb The action taken by the player.
	 * @param noun The subject of the action.
	 * @param target The target (indirect object) of the action.
	 * @return <code>true</code> if a valid action was found and executed,
	 *         <code>false</code> otherwise.
	 */
	public boolean executeScriptAction(Verb verb, Noun noun, Noun target)
	{
		this.noun = noun;
		boolean executed = false;
		for (int i = 0; i < scriptActions.size(); ++i)
		{
			ScriptAction action = (ScriptAction) scriptActions.elementAt(i);
			if (action.getVerb().equals(verb))
			{
				if (action.matchesNoun(noun))
				{
					if (action.matchesTarget(target))
					{
						if (action.isPreRequisiteSatisfied())
						{
							action.executeResults();
							executed = true;
							break;
						}
					}
				}
			}
		}
		// The above gives us a chance to do special handling for movement.
		// In the normal case, we simply update the player's position.
		if (!executed)
		{
			if (verb instanceof Direction)
			{
				player.setCurrentLocation(getRoom(player.getCurrentLocation()
						.getExit((Direction) verb)));
				executed = true;
			}
		}
		return executed;
	}

	/**
	 * Writes the Game data and current state to an output stream. This provides
	 * a Serializable-esque interface to the Game, even though Serializable may
	 * not always be available (e.g., if running on a J2ME device).
	 * 
	 * @param out The stream that will receive the formatted game data.
	 * @throws IOException
	 */
	public void writeObject(DataOutputStream out) throws IOException
	{
		// The ID is used to uniquely identify the game. It comes first so that
		// other
		// classes and tools can identify it.
		out.writeUTF(id);
		// The version is used to distinguish between multiple, incompatible
		// instances of
		// the same Game. This tag can be used to keep the player from loading
		// an
		// incompatible version of the game.
		out.writeInt(version);
		// Write out the player's location.
		out.writeUTF(player.getCurrentLocation().toString());
		Enumeration e = items.elements();
		while (e.hasMoreElements())
		{
			// Write out each item's name and location so the game state can be
			// properly
			// restored. The exception is AnyNoun, which is included in the list
			// of
			// items but is not actually located anywhere.
			Noun noun = (Noun) e.nextElement();
			if (noun instanceof AnyNoun)
			{
				continue;
			}
			out.writeUTF(noun.toString());
			out.writeUTF(noun.getLocation().toString());
		}
		// Note that Script Actions are not serialized. This is because the
		// actions are
		// not manipulated during the course of a game, and so they can just be
		// loaded from
		// the original game data.
	}

	/**
	 * Reads the Game data and current state from the input stream. This
	 * provides a Serializable-esque interface to the Game, even though
	 * Serializable may not always be available (e.g., if running on a J2ME
	 * device).
	 * 
	 * @param in The source of the game data.
	 * @throws IOException
	 */
	public void readObject(DataInputStream in) throws IOException
	{
		id = in.readUTF();
		version = in.readInt();
		player.setCurrentLocation(getRoom(in.readUTF()));
		while (in.available() > 0)
		{
			String itemName = in.readUTF();
			Noun item = getNoun(itemName);
			String locationName = in.readUTF();
			// There are a few special locations - treat those appropriately.
			if (locationName.equalsIgnoreCase(Game.INVENTORY_STRING))
			{
				player.addItem(item);
			} else if (locationName.equalsIgnoreCase(Nowhere.NOWHERE))
			{
				Nowhere.getInstance().addItem(item);
			} else
			{
				Room room = getRoom(locationName);
				if (room == null)
				{
					// This may be hit during development, but shouldn't ever
					// happen
					// on a completed game.
					System.err.println("Bad room name: " + locationName);
					throw new IOException("Bad room name: " + locationName);
				}
				room.addItem(item);
			}
		}
	}

	/**
	 * Returns the unique ID for this game. This can be used to distinguish
	 * between different possible games, to correspond save game files with game
	 * data, and so on.
	 * 
	 * @return A String describing the name of this game.
	 */
	public String getID()
	{
		return id;
	}

	/**
	 * Sets the unique ID for this game.
	 * 
	 * @param name The unique name for this game.
	 */
	public void setID(String name)
	{
		this.id = name;
	}

	/**
	 * Retrieves the version for this game. This can be used to determine
	 * whether a save game's data is valid to be read.
	 * 
	 * @return The game version.
	 */
	public int getVersion()
	{
		return version;
	}

	/**
	 * Sets the version of this game.
	 * 
	 * @param version The version number. Higher numbers indicate more recent
	 *            versions.
	 */
	public void setVersion(int version)
	{
		this.version = version;
	}

	// Package accessor. This allows script actions to peek back and see what's
	// currently being processed.
	Noun getCurrentNoun()
	{
		return noun;
	}

	/**
	 * Resets the game state, removing all game data.
	 */
	public void reset()
	{
		directions = new Hashtable();
		rooms = new Hashtable();
		items = new Hashtable();
		verbs = new Hashtable();
		scriptActions = new Vector();
		player = new Player();
	}

}
